/**
* Created with CLion.
* User:Lenovo
* Date:2023-03-28
* Time:18:43
* Description:文件操作
*/

#include <stdio.h>
#include <windows.h>

struct Sp
{
    char name[20];
    int age;
    float score;
};

struct S
{
    int n;
    float f;
    char arr[20];
};

int main() {
    //文件：存放在硬盘上，可以持久化的保存起来
    //分类：程序文件(.c，.obj(目标文件)，.exe ...)，数据文件(.txt ...)
    //程序文件读取/写入数据文件
    //文件名：文件路径 + 文件名主干 + 文件后缀
    //例如：C:\code\test.txt

    //文件的打开和关闭
    //文件指针
    {
        //每个被使用的文件都在内存中开辟了一个相应的文件信息区，用来存放文件的相关信息
        //这些信息是保存在一个结构体变量中的，这个结构体变量由系统声明，取名 FILE
        //一般通过 FILE 类型的指针来管理文件执行操作
        // ANSI C 规定，fopen 打开文件，fclose 关闭文件

        // fopen - FILE *fopen(const char *filename, const char *mode)
        // filename 文件名，mode 打开方式("r" "w" "a" "r+" "w+" "a+" ...)
        // "r" 只读，为了输入数据，打开一个已经存在的文本文件 - 文件不存在就会出错
        // "w" 只写，为了输出数据，打开一个文本文件 - 无论文件存不存在都会创建一个新的文件并销毁原来的文件
        // "a" 追加，向文本文件尾添加数据 - 如果文件不存在就会建立一个新的文件
        // "rb" 只读，二进制形式
        // "wb" 只写，二进制形式
        // "ab" 追加，二进制形式
        // "r+" "w+" "a+" 读写
        // "rb+" "wb+" "ab+" 读写，二进制形式

        {
            //打开文件
            FILE *pf = fopen("test.txt", "r");//文件信息区的地址在 pf 中
            //如果文件不存在，返回空指针
            if (pf == NULL) {
                perror("fopen");// fopen: No such file or directory
            }
            // test.txt 是相对路径，保存在项目的路径下
            //如果要读取/写入其他地方的文件，要使用绝对路径
            //写文件


            //关闭文件
            fclose(pf);
            pf = NULL;//指针置空
        }
    }

    //顺序读写 - 输入和输出是按照内存的角度看的
    {
        // fgetc - int fgetc(FILE *stream)                           字符输入，读取错误返回 EOF(-1)
        // fputc - int fputc(int character, FILE *stream)            字符输出
        // fgets - char *fgets(char *str, int num, FILE *stream)     文本行输入
        // fputs - int fputs(const char *str, FILE *stream)          文本行输出
        // fscanf - fscanf(FILE *stream, const char *format, ...)    格式化读取
        // fprintf - fprintf(FILE *stream, const char *format, ...)  格式化输出
        // fread     二进制读取
        // fwrite    二进制输出
        //流：数据的输入输出类似于水流，输入的数据输入到流中，输出的数据从流中读取
        //读写文件的时候：文件流
        //终端设备 - 屏幕：标准输出流    stdout
        //          键盘：标准输入流    stdin
        //          屏幕：标准错误流    stderr
        // C 语言程序默认打开上面三个流 - 不需要打开键盘、屏幕
        //除了 fread，fwrite 只支持文件流，剩下的所有函数都支持所有输入/输出流
        FILE *pf = fopen("test.txt", "r");// "w"
        if (pf == NULL)
        {
            perror("fopen");
            return 1;
        }
        //写文件 - "w"
        //把 26 个字母写入到文件中
        for (int i = 0; i < 26; i++)
        {
            fputc('a' + i, pf);
        }
        //读文件 - "r"
        for (int i = 0; i < 26; i++)
        {
            int ch = fgetc(pf);
            if (ch == -1)
            {
                break;
            }
            printf("%c ", ch);// abcdef ...
        }
        printf("\n");
        //为什么不是 aaaaaaaa ... ?
        //读取完 a 之后文件指针会向后挪动一步
        /* pf++ */ // err - 把文件信息区向后走一位

        //关闭文件
        fclose(pf);
        pf = NULL;

        pf = fopen("test.txt", "r");// "w"
        if (pf == NULL)
        {
            perror("fopen");
            return 1;
        }
        fputs("hello bit\n", pf);// "w"
        fputs("hello world", pf);// "w"

        char arr[20];
        fgets(arr, 5, pf);//实际上读的是 4 个 - 最后一个是 '\0'
        //第一个参数是要把读取的东西放到哪
        //第二个参数是最多读入几个字符
        //第三个参数是文件指针
        printf("%s\n", arr);// hell

        fclose(pf);
        pf = NULL;

        //格式化的读写 - fscanf, fprintf
        {
            struct S s = {100, 3.14F, "zhangsan"};
            pf = fopen("test.txt", "r");
            if (pf == NULL)
            {
                perror("fopen");
                return 1;
            }

            fprintf(pf, "%d %f %s", s.n, s.f, s.arr);

            struct S s0 = {0};
            fscanf(pf, "%d %f %s", &(s.n), &(s.f), s.arr);
            printf("%d %f %s\n", s.n, s.f, s.arr);// 100 3.140000 zhangsan

            fclose(pf);
            pf = NULL;
        }

        // fgetc / fgets 读取标准输入流，fputc，fputs 输出标准输出流
        {
            // stdin stdout stderr 都是 FILE *
            int ch = fgetc(stdin);
            printf("%c\n", ch);// a
            fputc(ch, stdout);// a
            printf("\n");

            struct S s = {1000, 3.666F, "hehe"};
            fprintf(stdout, "%d %f %s", s.n, s.f, s.arr);

            struct S s0 = {0};
            fscanf(stdin, "%d %f %s", &(s0.n), &(s0.f), s0.arr);
            fprintf(stdout, "%d %f %s\n", s0.n, s0.f, s0.arr);
        }
    }

    //对比一组函数：
    // scanf - 针对标准输入流(stdin)的格式化输入函数
    // printf - 针对标准输出流(stdout)的格式化输出函数
    // fscanf - 针对所有输入流(文件流/stdin)的格式化输入函数
    // fprintf - 针对所有输出流(文件流/stdout)的格式化输出函数
    // sscanf - int sprintf(char *str, const char *format, ...)
    //从字符串里提取格式化的数据
    // sprintf - int sprintf(char *str, const char *format, ...) 可变参数
    //把一个格式化的数据转换成字符串
    {
        struct S s = {200, 3.5F, "wangwu"};
        char arr[200];
        sprintf(arr, "%d %f %s", s.n, s.f, s.arr);
        printf("字符串的数据：%s\n", arr);

        struct S s0 = {0};
        sscanf(arr, "%d %f %s", &(s0.n), &(s0.f), s0.arr);
        printf("格式化的数据：%d %f %s\n", s0.n, s0.f, s0.arr);
    }
    //序列化和反序列化的时候用

    //二进制的写文件
    // fwrite - size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream)
    // fread - size_t - fread(void *ptr, size_t size, size_t count, FILE *stream)
    //(要写入/读取的)数据的地址，一个元素有多大，写多少个元素，文件指针
    //返回实际读取到的完整元素的个数
    {
        struct Sp sp = {"zhangsan", 20, 95.5F};
        struct Sp sp0 = {0};
        FILE *pf = fopen("text.dat", "rb");
        if (pf == NULL)
        {
            perror("fopen");
            return 1;
        }

        fwrite(&sp, sizeof(struct Sp), 1, pf);//二进制的写 - "wb"
        fread(&sp0, sizeof(struct Sp), 1, pf);
        printf("%s %d %f\n", sp0.name, sp0.age, sp0.score);
    }

    //文件的随机读写
    // fseek - int fseek(FILE *stream, long int offset, int origin)
    // offset 偏移量，origin 起始位置
    // origin 有三种选择：
    // 1.文件的开始位置 SEEK_SET
    // 2.文件指针的当前位置 SEEK_CUR
    // 3.文件的末尾 SEEK_END
    //偏移量和起始位置相对应，是你想要移动到的位置和 origin 的位置之差
    //根据文件指针的位置和偏移量定位(移动)文件指针
    {
        FILE *pf = fopen("test.txt", "r");
        if (pf == NULL)
        {
            perror("fopen");
        }
        int ch = fgetc(pf);
        printf("%c\n", ch);// 1
        ch = fgetc(pf);
        printf("%c\n", ch);// 0
        ch = fgetc(pf);
        printf("%c\n", ch);// 0
        ch = fgetc(pf);
        printf("%c\n", ch);// ' '

        fseek(pf, -3, SEEK_CUR);
        ch = fgetc(pf);
        printf("%c\n", ch);// 0
        fseek(pf, 1, SEEK_SET);
        ch = fgetc(pf);
        printf("%c\n", ch);// 0

        fclose(pf);
        pf = NULL;
    }

    // ftell - long int ftell(FILE *stream)
    //返回文件指针相对于起始位置的偏移量
    // rewind - void rewind(FILE *stream)
    //让文件指针回归到起始位置
    {
        FILE *pf = fopen("test.txt", "r");
        if (pf == NULL)
        {
            perror("fopen");
        }
        fseek(pf, 4, SEEK_SET);
        printf("%ld\n", ftell(pf));// 4
        rewind(pf);
        printf("%ld\n", ftell(pf));// 0
        int ch = fgetc(pf);
        printf("%c\n", ch);// 1

        fclose(pf);
        pf = NULL;
    }

    //二进制文件和文本文件
    //数据在内存中以二进制的形式存储，如果不加转换的输出到外存，就是二进制文件
    //如果要求在外存上以 ASCII 码的形式存储，则需要在存储前转换，以 ASCII 字符的形式存储的文件就是文本文件

    //文件读取结束的判定
    {
        //被错误使用的 feof
        // feof 的作用是已经读取结束，判断为什么结束(遇到文件末尾/读取错误)

        //正确判断的方式
        // fgetc 函数返回值的分析：返回 EOF
        // 1.遇到文件末尾，这时候会同时设置一个状态表示遇到文件末尾了，使用 feof 来检测这个状态
        // 2.遇到错误，同时设置一个状态表示遇到了错误，使用 ferror 来检测这个状态

        FILE *pf = fopen("test.txt", "r");
        if (pf == NULL)
        {
            perror("fopen");
            return 1;
        }
        int ch;
        while ((ch = fgetc(pf)) != EOF)
        {
            printf("%c", ch);
        }
        printf("\n");
        if (ferror(pf))
        {
            printf("I/O error when reading\n");
        }
        else if (feof(pf))
            // #define	__sfeof(p)	((int)(((p)->_flags & __SEOF) != 0))*
        {
            printf("End of file reached successfully\n");
        }

        fclose(pf);
        pf = NULL;

        //文本文件读取是否结束，判断返回值是否为 EOF(fgetc) 或是 NULL(fgets)
        //二进制文件读取是否结束，判断返回值是否小于实际要读的个数
    }

    //文件缓冲区
    //写入/读取文件的时候，在硬盘和文件中间有一个区域叫做缓冲区
    //不是直接写入，而是先放到缓冲区中，只有缓冲区满或者调用特殊方法才能刷新缓冲区
    // fflush 刷新，fclose 也会刷新
    {
        FILE *pf = fopen("Test2.txt", "w");
        fputs("abcdef", pf);//先将代码放在缓冲区
        printf("睡眠 10 秒 - 已经写数据了，打开 test2.txt 文件，发现没有内容\n");
        Sleep(10000);
        printf("刷新缓冲区\n");
        fflush(pf);//刷新缓冲区时，才能将输出缓冲区的数据写到文件
        printf("再睡眠 10 秒 - 此时，再次打开 test2.txt 文件，文件有内容了\n");
        Sleep(10000);

        fclose(pf);
        pf = NULL;
    }

    //实现一个代码，拷贝一个文件
    {
        FILE *src = fopen("test1.txt", "r");
        FILE *dest = fopen("Test3.txt", "w");
        if (!(dest && src))
        {
            perror("fopen");
        }
        int tmp;
        while ((tmp = fgetc(src)) != EOF)
        {
            fputc(tmp, dest);
        }
        fclose(src);
        src = NULL;
        fclose(dest);
        dest = NULL;
    }

    return 0;
}
